package fr.labri.harmony.analysis.jsep;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.text.DecimalFormat;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import fr.labri.harmony.core.analysis.AbstractAnalysis;
import fr.labri.harmony.core.config.model.AnalysisConfiguration;
import fr.labri.harmony.core.dao.Dao;
import fr.labri.harmony.core.model.Author;
import fr.labri.harmony.core.model.Data;
import fr.labri.harmony.core.model.Event;
import fr.labri.harmony.core.model.Source;
import fr.labri.harmony.core.source.WorkspaceException;
import fr.labri.scanlib.DataLib;
import fr.labri.scanlib.ScanLib;

public class JsepAnalysis extends AbstractAnalysis {

	AtomicInteger cpt = new AtomicInteger();
	static final int MIN_COMMIT = 10;
	static final int MIN_MONTH = 1;
	static final int MIN_CLOC = 100;
	
	public JsepAnalysis() {
		super();
	}

	public JsepAnalysis(AnalysisConfiguration config, Dao dao, Properties properties) {
		super(config, dao, properties);
//		ScanLib.getInstance().add("org.lib1","lib1");
//		ScanLib.getInstance().add("org.lib2","lib2");
//		ScanLib.getInstance().add("org.lib3","lib3");
//		ScanLib.getInstance().add("org.lib4","lib4");
//		ScanLib.getInstance().add("org.lib5","lib5");
		ScanLib.buildFromFile("/home/cteyton/Harmony.JSEP/fr.labri.harmony.analysis.jsep/scanlib-data.xml");
		ScanLib.getInstance().remove("ome");
		ScanLib.getInstance().remove("ion.");
		ScanLib.getInstance().remove("java.sql.");
		ScanLib.getInstance().remove("play.");
		System.out.println(ScanLib.getInstance().getDatabaseContent().size()+" libraries");
	}

	@Override
	public void runOn(Source src) {
		AnalysisSource as = new AnalysisSource(src);
		as.perform();
		as = null;
	}


	class AnalysisSource {
		//Maintain index of events and number
		Map<Integer,Event> index_events = new HashMap<Integer, Event>();
		//Maintain the cloc for a version
		Map<String,Integer> index_cloc = new HashMap<String, Integer>();
		//Maintain the number of devs for a version
		Map<String,Integer> index_devs = new HashMap<String, Integer>();
		//Maintain the number of devs for a version
		Map<String,Set<String>> index_nbLibs = new HashMap<String, Set<String>>();
		//Maintain the data for the library used

		Source src;
		String workdir;

		AnalysisSource(Source src) {
			this.src = src;
			this.workdir = src.getWorkspace().getPath();
		}

		void perform() {
			//Store the results
			Set<CandidateMigration> candidates = new HashSet<CandidateMigration>();

			List<Event> events = linearizeHistory(src);
			if(events.size() < MIN_COMMIT)
				return;
			int nbM = getNumberOfMonths(events.get(0).getTimestamp(), events.get(events.size()-1).getTimestamp());
			if(nbM < MIN_MONTH) 
				return;
			String workdir = src.getWorkspace().getPath();
			int cloc = getJavaCloc(events.get(events.size()-1), events.get(events.size()-1));
			if(cloc  < MIN_CLOC) 
				return;
			
			//Save url as a valid project
			try{
			FileWriter fw = new FileWriter("computed_projects.txt",true);
			fw.write(src.getUrl()+"\n");
			fw.close();
			}
			catch(IOException e){}
			
			int nbAuthors = getNumberOfAuthors(src.getAuthors());
			List<DataLib> baseDataLibs = ScanLib.getInstance().computeLibrariesWithData(workdir);
			Set<String> baseLibs = new HashSet<String>();
			for(DataLib lib : baseDataLibs) 
				baseLibs.add(lib.getLibrary());
			
			//Maintain the previous libs
			Set<String> prevLibs = new HashSet<>();
			Map<String,Integer> createdLibs = new HashMap<String, Integer>();


			List<LibraryUsed> libUsed = new ArrayList<LibraryUsed>();
			//Maintain the datalib per event per lib
			Map<String,Map<String,DataLib>> events_dataLib = new HashMap<String, Map<String,DataLib>>();
			long timeCheckout = 0L;
			long timeDep = 0L;
			for(int cptEvent = 0 ; cptEvent < events.size() ; cptEvent++) {
			
				Event e = events.get(cptEvent);
				index_events.put(cptEvent, e);
				//Synchro
				long tmp = System.currentTimeMillis();
				src.getWorkspace().update(e);
				timeCheckout += System.currentTimeMillis() - tmp;
				//Compute currents libs
				long dep = System.currentTimeMillis();
				List<DataLib> currentDataLibs = ScanLib.getInstance().computeLibrariesWithData(workdir);
				Set<String> currentLibs = new HashSet<String>();
				events_dataLib.put(e.getNativeId(), new HashMap<String, DataLib>());
				for(DataLib lib : currentDataLibs) {
					currentLibs.add(lib.getLibrary());
					events_dataLib.get(e.getNativeId()).put(lib.getLibrary(), lib);
				}
				index_nbLibs.put(e.getNativeId(), currentLibs);
				//Compute added libs
				Set<String> added = new HashSet<String>(currentLibs);
				added.removeAll(prevLibs);
				for(String add : added) {
					createdLibs.put(add, cptEvent);
				}
				//Compute removed libs
				Set<String> removed = new HashSet<String>(prevLibs);
				removed.removeAll(currentLibs);	
				//Compute candidates
				for(String rem : removed) {
					LibraryUsed us = new LibraryUsed(events.size(),cloc,nbM,nbAuthors,baseLibs);
					Event intro = index_events.get(createdLibs.get(rem));
					us.setName(rem);
					us.setProject(src.getUrl());
					us.setSize_in(getJavaCloc(e, intro));
					us.setSize_out(getJavaCloc(e, e));
					us.setNb_dev_at(getNumberOfAuthors(intro, events));
					us.setNb_dev_out(getNumberOfAuthors(e, events));
					us.setIntro_at(intro.getNativeId());
					us.setIntro_at_number(createdLibs.get(rem));
					us.setLibs_at(index_nbLibs.get(intro.getNativeId()).size());
					us.setLibs_set_at(index_nbLibs.get(intro.getNativeId()).toString());
					us.setLibs_out(index_nbLibs.get(e.getNativeId()).size());
					us.setLibs_set_out(index_nbLibs.get(e.getNativeId()).toString());
					us.setIntro_out(e.getNativeId());
					us.setIntro_out_number(cptEvent);
					us.setTimestamp_at(intro.getTimestamp());
					us.setTimestamp_out(e.getTimestamp());
					for(DataLib lib : currentDataLibs) {
						String cur = lib.getLibrary();
						if(createdLibs.get(cur) > createdLibs.get(rem)) {
							Set<Author> authors = new HashSet<Author>();
							List<String> logs = new ArrayList<String>();
							int k = 0;
							for(k = createdLibs.get(rem) + 1 ; k <= cptEvent ; k++) {
								authors.addAll(index_events.get(k).getAuthors());
								logs.add(index_events.get(k).getMetadata().get("COMMIT_LOG"));
							}
							Event intro_cur = index_events.get(createdLibs.get(cur)-1);
							//candidates.add(new CandidateMigration(rem, cur, index_events.get(createdLibs.get(cur)-1).getNativeId(), index_events.get(cptEvent).getNativeId(),interm));
							LibraryUsed lu = new LibraryUsed(us);
							lu.setMigration_lib_to(cur);
							lu.setMigration_length(cptEvent-createdLibs.get(cur)+1);
							lu.setMigration_cloc_in(getJavaCloc(e, intro_cur));
							lu.setMigration_devs_in(getNumberOfAuthors(intro_cur, events));
							lu.setMigration_dev_contrib(authors.size());
							lu.setMigration_reason(logs.toString());
							lu.setMigration_libs_in(index_nbLibs.get(intro_cur.getNativeId()).size());
							lu.setMigration_libs_set_in(index_nbLibs.get(intro_cur.getNativeId()).toString());
							lu.setMigration_start(intro_cur.getNativeId());
							lu.setMigration_start_number(createdLibs.get(cur)-1);
							lu.setMigration_timestamp_in(intro_cur.getTimestamp());
							lu.setMigration_files_set_in(events_dataLib.get(intro_cur.getNativeId()).get(rem).getFiles().toString());
							lu.setFiles_set_out(events_dataLib.get(e.getNativeId()).get(cur).getFiles().toString());
							lu.setMigration_nb_fichiers_in(events_dataLib.get(intro_cur.getNativeId()).get(rem).getNbFiles());
							lu.setMigration_total_fichiers_in(events_dataLib.get(intro_cur.getNativeId()).get(rem).getTotalFiles());
							lu.setMigration_ratio_fichiers_in(events_dataLib.get(intro_cur.getNativeId()).get(rem).getRatio());
							lu.setMigration_nb_fichiers_out(events_dataLib.get(e.getNativeId()).get(cur).getNbFiles());
							lu.setMigration_ratio_fichiers_out(events_dataLib.get(e.getNativeId()).get(cur).getRatio());
							lu.setMigration_total_fichiers_out(events_dataLib.get(e.getNativeId()).get(cur).getTotalFiles());
							libUsed.add(lu);
						}
					}
					createdLibs.remove(rem);
					libUsed.add(us);
				}
				//Update prev index
				prevLibs = currentLibs;
				timeDep += System.currentTimeMillis() - dep;
			}
			//conclusion
			for(String lib : createdLibs.keySet()) {
				Event last = events.get(events.size()-1);
				LibraryUsed us = new LibraryUsed(events.size(),cloc,nbM,nbAuthors,baseLibs);
				Event intro = index_events.get(createdLibs.get(lib));
				us.setName(lib);
				us.setProject(src.getUrl());
				us.setSize_in(getJavaCloc(last, intro));
				us.setSize_out(getJavaCloc(last, last));
				us.setNb_dev_at(getNumberOfAuthors(intro, events));
				us.setNb_dev_out(getNumberOfAuthors(last, events));
				us.setIntro_at(intro.getNativeId());
				us.setIntro_at_number(createdLibs.get(lib));
				us.setLibs_at(index_nbLibs.get(intro.getNativeId()).size());
				us.setLibs_set_at(index_nbLibs.get(intro.getNativeId()).toString());
				us.setLibs_out(index_nbLibs.get(last.getNativeId()).size());
				us.setLibs_set_out(index_nbLibs.get(last.getNativeId()).toString());
				us.setIntro_out(last.getNativeId());
				us.setIntro_out_number(events.size()-1);
				us.setTimestamp_at(intro.getTimestamp());
				us.setTimestamp_out(last.getTimestamp());
				us.setPresence_end(true);
				libUsed.add(us);
			}
			double moyCheck = (double)timeCheckout/(double)events.size();
			double moyDep = (double)timeDep/(double)events.size();
			try {
				FileWriter fw = new FileWriter("jsep_algo.csv",true);
				fw.append(src.getUrl()+";"+events.size()+";"+formatDouble(moyCheck)+";"+formatDouble(moyDep)+";"+prevLibs.size()+";"+candidates.size()+"\n");
				fw.close();
			}
			catch(IOException e){}

			for(LibraryUsed l : libUsed) {
				dao.saveData(JsepAnalysis.this, l, Data.SOURCE, src.getId());
			}

			//			ProjectData pd = new ProjectData(
			//					getNumberOfMonths(events.get(0).getTimestamp(),events.get(events.size()-1).getTimestamp()),
			//					events.get(0).getTimestamp(), 
			//					events.get(events.size()-1).getTimestamp(), 
			//					events.size(),
			//					getJavaCloc(workdir),
			//					getNumberOfAuthors(src.getUrl()), 
			//					new ArrayList<String>(prevLibs));
			//
			//			dao.saveData(this, pd, Data.SOURCE, src.getId());
			//
			//			for(CandidateMigration cm : candidates) {
			//				dao.saveData(this, cm, Data.SOURCE, src.getId());
			//			}
			//
			//			cpt.addAndGet(1);
			//			if(cpt.get()%100==0) {
			//				System.out.println("*********************");
			//				System.out.println(cpt.get());
			//				System.out.println("*********************");
			//			}
		}


		private List<Event> linearizeHistory(Source src) {
			List<Event> events = new ArrayList<Event>(src.getEvents());
			Collections.sort(events, new EventComparator());

			List<Event> oneBranch = new ArrayList<Event>();

			Event head = (Event) events.get(events.size() - 1);
			oneBranch.add((Event) events.get(events.size() - 1));
			Event par;
			while (!head.getParents().isEmpty()) {
				if (head.getParents().size() > 1) {
					int size = -1;
					List<Event> parts = new ArrayList<Event>(head.getParents());
					int max = 0;
					for (int i = 0; i < parts.size(); i++) {
						par = (Event) parts.get(i);
						if (size == -1) {
							size = head.getActions(par).size();
						} else if (size > head.getActions(par).size()) {
							max = 1;
						}
					}

					oneBranch.add((Event) parts.get(max));
					head = (Event) parts.get(max);
				} else {
					Event t = (Event) head.getParents().iterator().next();
					oneBranch.add(t);
					head = t;
				}
			}

			Collections.reverse(oneBranch);
			return oneBranch;
		}

		private String formatDouble(double s) {
			return new DecimalFormat("#.##").format(s);
		}

		private int getJavaCloc(Event current, Event e) {
			if(index_cloc.containsKey(e.getNativeId()))
				return index_cloc.get(e.getNativeId());
			try {
				src.getWorkspace().update(e);
				Process p = Runtime.getRuntime().exec(new String[] { "cloc", new File(workdir).getAbsolutePath(), "--xml", "--quiet", "--force-lang=\"JAVA\"" });
				BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()));
				StringBuffer b = new StringBuffer();
				String line;
				while ((line = r.readLine()) != null) {
					if (line.trim().startsWith("<")) b.append(line + "\n");
				}
				p.waitFor();
				r.close();

				if(b.toString().isEmpty())
					return 0;

				DocumentBuilderFactory df = DocumentBuilderFactory.newInstance();
				DocumentBuilder db = df.newDocumentBuilder();

				Document doc = db.parse(new InputSource(new StringReader(b.toString().trim())));
				NodeList l = doc.getElementsByTagName("language");
				src.getWorkspace().update(current);
				if(l.getLength()>0){
					Node el = l.item(0);
					final NamedNodeMap attributes = el.getAttributes();
					int cloc = Integer.parseInt(attributes.getNamedItem("code").getTextContent());
					index_cloc.put(e.getNativeId(), cloc);
					return cloc;
				}
				p.waitFor();
			} catch (Exception ex) {
				throw new WorkspaceException(ex);
			}
			return 0;
		}

		private int getNumberOfMonths(long start, long end) {
			Calendar startCalendar = new GregorianCalendar();
			startCalendar.setTime(new Date(start*1000));
			Calendar endCalendar = new GregorianCalendar();
			endCalendar.setTime(new Date(end*1000));

			int diffYear = endCalendar.get(Calendar.YEAR) - startCalendar.get(Calendar.YEAR);
			int diffMonth = diffYear * 12 + endCalendar.get(Calendar.MONTH) - startCalendar.get(Calendar.MONTH);
			return diffMonth;
		}

		/*
		 * Return the number of developers - identities have been merged
		 */


		private Integer getNumberOfAuthors(Event e, List<Event> events) {
			if(index_devs.containsKey(e.getNativeId()))
				return index_devs.get(e.getNativeId());
			Set<Author> authors = new HashSet<Author>();
			for(int i = 0 ; i < events.size() ; i++) {
				authors.addAll(events.get(i).getAuthors());
				if(events.get(i).getNativeId().equals(e.getNativeId()))
					break;
			}
			int total =  getNumberOfAuthors(new ArrayList<>(authors));;
			index_devs.put(e.getNativeId(),total);
			return total;
		}


		private int getNumberOfAuthors(List<Author> authors) {
			List<Author> copy = new ArrayList<Author>();

			for(Author dev : authors) {
				String name = clean(dev.getName());
				dev.setName(name);
			}

			int newSize = authors.size();
			int oldSize = 0;
			while(oldSize != newSize) {
				for(int i = 0 ; i < authors.size() ; i++) {
					Author dev = authors.get(i);
					for(int j = i ; j < authors.size() ; j++) {
						Author dev2 = authors.get(j);
						if(dev.getId()!=dev2.getId()) {
							if(dev.getName().equals(dev2.getName())) {
								copy.add(dev);
								copy.add(dev2);
								break;
							}
							else {
								String s1 = dev.getEmail().toLowerCase().replaceAll("\\.", "").replaceAll("\\-", "");
								String s2 = dev2.getEmail().toLowerCase().replaceAll("\\.", "").replaceAll("\\-", "");

								if(s1.contains("@") && s2.contains("@")) {
									s1 = s1.substring(0,s1.indexOf("@"));
									s2 = s2.substring(0,s2.indexOf("@"));
									if(s1.equals(s2)){
										copy.add(dev);
										copy.add(dev2);
										break;
									}
								}

							}
						}
					}
				}
				oldSize = authors.size();
				for(int j = 0 ; j < copy.size() ; j=j+2 ){
					if(copy.get(j).getName().length() > copy.get(j+1).getName().length()) {
						authors.remove(copy.get(j+1));
					}
					else {
						authors.remove(copy.get(j));
					}
				}
				newSize = authors.size();
			}
			return authors.size();
		}

		private String clean(String name) {
			String n = "";
			name = Normalizer.normalize(name, Normalizer.Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
			name = name.replaceAll("\\_", "\\.").replaceAll("\\?", "");
			for(int i = 0 ; i < name.length() ; i++)
				if(i > 0 && name.charAt(i-1)==' ')
					n+= Character.toUpperCase(name.charAt(i));
				else
					n+= name.charAt(i);
			return n;
		}
	}

	class EventComparator implements Comparator<Event> {

		@Override
		public int compare(Event arg0, Event arg1) {
			return Long.compare(arg0.getTimestamp(), arg1.getTimestamp());
		}	
	}	

}
